package com.ut.richpath;

import com.ut.richpath.listener.OnRichPathUpdatedListener;
import com.ut.richpath.model.Group;
import com.ut.richpath.pathparser.PathDataNode;
import com.ut.richpath.pathparser.PathParser;
import com.ut.richpath.pathparser.PathParserCompat;
import com.ut.richpath.util.JsonUtil;
import com.ut.richpath.util.LogUtil;
import com.ut.richpath.util.PathUtils;
import com.ut.richpath.util.XmlParser;
import ohos.agp.render.*;
import ohos.agp.utils.Color;
import ohos.agp.utils.Matrix;
import ohos.app.Context;
import ohos.app.dispatcher.TaskDispatcher;
import ohos.global.resource.solidxml.Attribute;
import ohos.org.xml.sax.Attributes;

import java.util.ArrayList;
import java.util.List;

public class RichPath extends Path{

    public static final String TAG_NAME = "path";

    private Path src;

    private TaskDispatcher taskDispatcher;

    private boolean setTrimPath = false;

    public RichPath(TaskDispatcher taskDispatcher, Path path) {
        super(path);
        this.src = path;
        this.taskDispatcher = taskDispatcher;
        init();
    }

    public RichPath(TaskDispatcher taskDispatcher, String pathData) {
        this(taskDispatcher, PathParser.createPathFromPathData(pathData));
    }

    private int fillColor = 0x00000000;
    
    private int strokeColor = 0x00000000;
    
    private float fillAlpha = 1f;
    private float strokeAlpha = 1f;
    private float strokeWidth = 0f;
    private float trimPathStart = 0f;
    private float trimPathEnd = 1f;
    private float trimPathOffset = 0f;

    private Paint.StrokeCap strokeLineCap = Paint.StrokeCap.BUTT_CAP;
    
    private Paint.Join strokeLineJoin = Paint.Join.MITER_JOIN;
    

    private float strokeMiterLimit = 4f;
    

    private String name;
    private Paint paint;
    private float rotation = 0f;
    private float scaleX = 1f;
    private float scaleY = 1f;
    private float translationX = 0f;
    private float translationY = 0f;

    private float originalWidth = 0f;
    private float originalHeight = 0f;
    private float pivotX = 0f;
    private float pivotY = 0f;
    private boolean isPivotToCenter = false;

    private OnRichPathUpdatedListener onRichPathUpdatedListener;

    private PathMeasure pathMeasure;

    /**
     * [pathDataNodes] couldn't be private.
     * It could called by @see [com.richpathanimator.AnimationBuilder.pathData]
     */
    public List<PathDataNode> pathDataNodes;

    private List<Matrix> matrices;

    private OnPathClickListener onPathClickListener;

    public int getFillColor() {
        return fillColor;
    }

    public void setFillColor(int fillColor) {
        this.fillColor = fillColor;
        onPathUpdated();
    }

    public int getStrokeColor() {
        return strokeColor;
    }

    public void setStrokeColor(int strokeColor) {
        this.strokeColor = strokeColor;
        onPathUpdated();
    }

    public float getFillAlpha() {
        return fillAlpha;
    }

    public void setFillAlpha(float fillAlpha) {
        this.fillAlpha = fillAlpha;
        onPathUpdated();
    }

    public float getStrokeAlpha() {
        return strokeAlpha;
    }

    public void setStrokeAlpha(float strokeAlpha) {
        this.strokeAlpha = strokeAlpha;
        onPathUpdated();
    }

    public float getStrokeWidth() {
        return strokeWidth;
    }

    public void setStrokeWidth(float strokeWidth) {
        this.strokeWidth = strokeWidth;
        onPathUpdated();
    }

    public float getTrimPathStart() {
        return trimPathStart;
    }

    public void setTrimPathStart(float trimPathStart) {
        this.trimPathStart = trimPathStart;
        trim();
        onPathUpdated();
    }

    public float getTrimPathEnd() {
        return trimPathEnd;
    }

    public void setTrimPathEnd(float trimPathEnd) {
        taskDispatcher.asyncDispatch(()->{
            this.trimPathEnd = trimPathEnd;
            setTrimPath = true;
            trim();
            onPathUpdated();
        });

    }

    public float getTrimPathOffset() {
        return trimPathOffset;
    }

    public void setTrimPathOffset(float trimPathOffset) {
        this.trimPathOffset = trimPathOffset;
        trim();
        onPathUpdated();
    }

    public Paint.StrokeCap getStrokeLineCap() {
        return strokeLineCap;
    }

    public void setStrokeLineCap(Paint.StrokeCap strokeLineCap) {
        this.strokeLineCap = strokeLineCap;
        onPathUpdated();
    }

    public Paint.Join getStrokeLineJoin() {
        return strokeLineJoin;
    }

    public void setStrokeLineJoin(Paint.Join strokeLineJoin) {
        this.strokeLineJoin = strokeLineJoin;
        onPathUpdated();
    }

    public float getStrokeMiterLimit() {
        return strokeMiterLimit;
    }

    public void setStrokeMiterLimit(float strokeMiterLimit) {
        this.strokeMiterLimit = strokeMiterLimit;
        onPathUpdated();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Paint getPaint() {
        return paint;
    }

    public void setPaint(Paint paint) {
        this.paint = paint;
    }

    public float getRotation() {
        return rotation;
    }

    public void setRotation(float rotation) {
        float deltaValue = rotation - this.rotation;
        LogUtil.i("rotation: %f, this.rotation: %f, deltaValue: %f", rotation, this.rotation, deltaValue);
        if (isPivotToCenter) {
            PathUtils.setPathRotation(this, deltaValue);
            PathUtils.setPathRotation(src, deltaValue);
        } else {
            PathUtils.setPathRotation(this, deltaValue, pivotX, pivotY);
            PathUtils.setPathRotation(src, deltaValue, pivotX, pivotY);
        }
        this.rotation = rotation;
        onPathUpdated();

    }

    public float getScaleX() {
        return scaleX;
    }

    public void setScaleX(float scaleX) {
        if (isPivotToCenter) {
            //reset scaling
            PathUtils.setPathScaleX(this, 1.0f / this.scaleX);
            PathUtils.setPathScaleX(src, 1.0f / this.scaleX);
            //new scaling
            PathUtils.setPathScaleX(this, scaleX);
            PathUtils.setPathScaleX(src, scaleX);
        } else {
            //reset scaling
            PathUtils.setPathScaleX(this, 1.0f / this.scaleX, pivotX, pivotY);
            PathUtils.setPathScaleX(src, 1.0f / this.scaleX, pivotX, pivotY);
            //new scaling
            PathUtils.setPathScaleX(this, scaleX, pivotX, pivotY);
            PathUtils.setPathScaleX(src, scaleX, pivotX, pivotY);
        }
        this.scaleX = scaleX;
        onPathUpdated();
    }

    public float getScaleY() {
        return scaleY;
    }

    public void setScaleY(float scaleY) {
        if (isPivotToCenter) { //reset scaling
            PathUtils.setPathScaleY(this, 1.0f / this.scaleY);
            PathUtils.setPathScaleY(src, 1.0f / this.scaleY);
            //new scaling
            PathUtils.setPathScaleY(this, scaleY);
            PathUtils.setPathScaleY(src, scaleY);
        } else { //reset scaling
            PathUtils.setPathScaleY(this, 1.0f / this.scaleY, pivotX, pivotY);
            PathUtils.setPathScaleY(src, 1.0f / this.scaleY, pivotX, pivotY);
            //new scaling
            PathUtils.setPathScaleY(this, scaleY, pivotX, pivotY);
            PathUtils.setPathScaleY(src, scaleY, pivotX, pivotY);
        }
        this.scaleY = scaleY;
        onPathUpdated();
    }

    public OnPathClickListener getOnPathClickListener() {
        return onPathClickListener;
    }

    public void setOnPathClickListener(OnPathClickListener onPathClickListener) {
        this.onPathClickListener = onPathClickListener;
    }

    public float getTranslationX() {
        return translationX;
    }

    public void setTranslationX(float translationX) {
        PathUtils.setPathTranslationX(this, translationX - this.translationX);
        PathUtils.setPathTranslationX(src, translationX - this.translationX);
        this.translationX = translationX;
        onPathUpdated();
    }

    public float getTranslationY() {
        return translationY;
    }

    public void setTranslationY(float translationY) {
        PathUtils.setPathTranslationX(this, translationY - this.translationY);
        PathUtils.setPathTranslationX(src, translationY - this.translationY);
        this.translationY = translationY;
        onPathUpdated();
    }

    public float getOriginalWidth() {
        return originalWidth;
    }

    public void setOriginalWidth(float originalWidth) {
        this.originalWidth = originalWidth;
    }

    public float getOriginalHeight() {
        return originalHeight;
    }

    public void setOriginalHeight(float originalHeight) {
        this.originalHeight = originalHeight;
    }

    public float getPivotX() {
        return pivotX;
    }

    public void setPivotX(float pivotX) {
        this.pivotX = pivotX;
    }

    public float getPivotY() {
        return pivotY;
    }

    public void setPivotY(float pivotY) {
        this.pivotY = pivotY;
    }

    public boolean isPivotToCenter() {
        return isPivotToCenter;
    }

    public void setPivotToCenter(boolean pivotToCenter) {
        isPivotToCenter = pivotToCenter;
    }

    public OnRichPathUpdatedListener getOnRichPathUpdatedListener() {
        return onRichPathUpdatedListener;
    }

    public void setOnRichPathUpdatedListener(OnRichPathUpdatedListener onRichPathUpdatedListener) {
        this.onRichPathUpdatedListener = onRichPathUpdatedListener;
    }

    public PathMeasure getPathMeasure() {
        return pathMeasure;
    }

    public void setPathMeasure(PathMeasure pathMeasure) {
        this.pathMeasure = pathMeasure;
    }

    public List<PathDataNode> getPathDataNodes() {
        return pathDataNodes;
    }

    public void setPathDataNodes(List<PathDataNode> pathDataNodes) {
        if (pathDataNodes==null)return;
        PathUtils.setPathDataNodes(this, pathDataNodes);
        this.pathDataNodes = pathDataNodes;
        for(Matrix matrix : matrices){
            transform(matrix);
        }
        onPathUpdated();
    }


    private void init() {
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE_STYLE);
        matrices = new ArrayList<>();
        updateOriginalDimensions();
    }

    public void setWidth(float width) {
        PathUtils.setPathWidth(this, width);
        PathUtils.setPathWidth(src, width);
        onPathUpdated();
    }

    public float getWidth() {
        return PathUtils.getPathWidth(this);
    }

    public void setHeight(float height) {
        PathUtils.setPathHeight(this, height);
        PathUtils.setPathHeight(src, height);
        onPathUpdated();
    }

    public float getHeight(){
        return PathUtils.getPathHeight(this);
    }

     public void draw(Canvas canvas) {
//        LogUtil.i("path: %s", JsonUtil.toJson(this));
        paint.setColor(new Color(applyAlpha(fillColor, fillAlpha)));//0xFFa4c639
        paint.setStyle(Paint.Style.FILL_STYLE);
        canvas.drawPath(src, paint);
        paint.setColor(new Color(applyAlpha(strokeColor, strokeAlpha)));
        paint.setStyle(Paint.Style.STROKE_STYLE);
        canvas.drawPath(src, paint);
        LogUtil.i("fill color: %s, stroke color: %s", Integer.toHexString(strokeColor), Integer.toHexString(applyAlpha(strokeColor, strokeAlpha)));
     }

    public void applyGroup(Group group) {
        mapToMatrix(group.matrix());
        pivotX = group.getPivotX();
        pivotY = group.getPivotY();
    }

    public void mapToMatrix(Matrix matrix) {
        matrices.add(matrix);
        transform(matrix);
        src.transform(matrix);
        mapPoints(matrix);
        updateOriginalDimensions();
    }

    public void mapPoints(Matrix matrix) {
        float[] src = {pivotX, pivotY};
        matrix.mapPoints(src);
        pivotX = src[0];
        pivotY = src[1];
    }

    public void scaleStrokeWidth(float scale) {
        paint.setStrokeWidth(strokeWidth * scale);
    }

    public void setPathData(String pathData) {
        pathDataNodes = PathParserCompat.createNodesFromPathData(pathData);
    }

    public void inflate(Context context, Attributes attributes) {
        String pathData = XmlParser.getAttributeString(attributes, "pathData", name);

        pathDataNodes = PathParserCompat.createNodesFromPathData(pathData);

        name = XmlParser.getAttributeString(attributes, "name", name);

        fillAlpha = XmlParser.getAttributeFloat(attributes, "fillAlpha", fillAlpha);

        fillColor = XmlParser.getAttributeColor(attributes, "fillColor", fillColor);

        strokeAlpha = XmlParser.getAttributeFloat(attributes, "strokeAlpha", strokeAlpha);

        strokeColor = XmlParser.getAttributeColor(attributes, "strokeColor", strokeColor);

        strokeLineCap = XmlParser.getAttributeStrokeLineCap(attributes, "strokeLineCap", strokeLineCap);

        strokeLineJoin = XmlParser.getAttributeStrokeLineJoin(attributes, "strokeLineJoin", strokeLineJoin);

        strokeMiterLimit = XmlParser.getAttributeFloat(attributes, "strokeMiterLimit", strokeMiterLimit);

        strokeWidth = XmlParser.getAttributeFloat(attributes, "strokeWidth", strokeWidth);

        trimPathStart = XmlParser.getAttributeFloat(attributes, "trimPathStart", trimPathStart);

        trimPathEnd = XmlParser.getAttributeFloat(attributes, "trimPathEnd", trimPathEnd);

        trimPathOffset = XmlParser.getAttributeFloat(attributes, "trimPathOffset", trimPathOffset);

        setFillType(XmlParser.getAttributePathFillType(attributes, "fillType", getFillType()));

        updatePaint();
        LogUtil.i("trim path end: %f", trimPathEnd);
        trim();
    }

    public void inflate(Context context, List<Attribute> attributes) {
        String pathData = XmlParser.getAttributeString(attributes, "pathData", name);

        pathDataNodes = PathParserCompat.createNodesFromPathData(pathData);

        name = XmlParser.getAttributeString(attributes, "name", name);

        fillAlpha = XmlParser.getAttributeFloat(attributes, "fillAlpha", fillAlpha);

        fillColor = XmlParser.getAttributeColor(attributes, "fillColor", fillColor);

        strokeAlpha = XmlParser.getAttributeFloat(attributes, "strokeAlpha", strokeAlpha);

        strokeColor = XmlParser.getAttributeColor(attributes, "strokeColor", strokeColor);

        strokeLineCap = XmlParser.getAttributeStrokeLineCap(attributes, "strokeLineCap", strokeLineCap);

        strokeLineJoin = XmlParser.getAttributeStrokeLineJoin(attributes, "strokeLineJoin", strokeLineJoin);

        strokeMiterLimit = XmlParser.getAttributeFloat(attributes, "strokeMiterLimit", strokeMiterLimit);

        strokeWidth = XmlParser.getAttributeFloat(attributes, "strokeWidth", strokeWidth);

        trimPathStart = XmlParser.getAttributeFloat(attributes, "trimPathStart", trimPathStart);

        trimPathEnd = XmlParser.getAttributeFloat(attributes, "trimPathEnd", trimPathEnd);

        trimPathOffset = XmlParser.getAttributeFloat(attributes, "trimPathOffset", trimPathOffset);

        setFillType(XmlParser.getAttributePathFillType(attributes, "fillType", getFillType()));

        updatePaint();
        LogUtil.i("trim path end: %f", trimPathEnd);
        trim();
    }

    private void updateOriginalDimensions() {
        originalWidth = PathUtils.getPathWidth(this);
        originalHeight = PathUtils.getPathHeight(this);
    }

    private void trim() {
        LogUtil.i("@trim path end: %f", this.trimPathEnd);
        if (trimPathStart != 0.0f || trimPathEnd != 1.0f || setTrimPath) {

            float start = (trimPathStart + trimPathOffset) > 0 ? (trimPathStart + trimPathOffset) % 1.0f : (trimPathStart + trimPathOffset);
            float end = (trimPathEnd + trimPathOffset) > 1 ? (trimPathEnd + trimPathOffset) % 1.0f : (trimPathEnd + trimPathOffset);
            PathMeasure pathMeasure = this.pathMeasure!=null ? this.pathMeasure : new PathMeasure(null, false);
            pathMeasure.setPath(this, false);
            float len = pathMeasure.getLength();
            start *= len;
            end *= len;
            LogUtil.i("path start: %f, path end: %f, start: %f, end: %f, length: %f", trimPathStart, trimPathEnd, start, end, len);
            src.reset();
            if (start > end) {
                pathMeasure.getSegment(start, len, src, true);
                pathMeasure.getSegment(0f, end, src, true);
            } else {
                pathMeasure.getSegment(start, end, src, true);
            }
            rLineTo(0f, 0f); // fix bug in measure
            setTrimPath = false;
        }
    }

    private void updatePaint() {
        paint.setStrokeCap(strokeLineCap);
        paint.setStrokeJoin(strokeLineJoin);
        paint.setStrokeMiter(strokeMiterLimit);
        paint.setStrokeWidth(strokeWidth);
        //todo fillType
    }

    private void onPathUpdated() {
        if (onRichPathUpdatedListener!=null){
            onRichPathUpdatedListener.onPathUpdated();
        }
    }

    private int applyAlpha(int color, float alpha){
        int alphaBytes = Color.alpha(color);
        int newColor = color&0x00FFFFFF;
        newColor = newColor|(((int)(alphaBytes * alpha))<<24);
        return newColor;
    }

    interface OnPathClickListener {
        void onClick(RichPath richPath);
    }
}
